﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Objects;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using MedicinaMuncii.DAL.Contracts;
using MedicinaMuncii.Entities;
using MedicinaMuncii.Repository.Contracts;
using System.Collections;
using System.Data.Common;
using MedicinaMuncii.IoC;

namespace MedicinaMuncii.Repository.Implementations
{
    /// <summary>
    /// Implementation of the IGenericRepository{T}
    /// </summary>
    /// <typeparam name="T">The <see cref="IObjectWithChangeTracker"/>.</typeparam>
    public class GenericRepository<T> : IGenericRepository<T> where T : class, IObjectWithChangeTracker
    {
        /// <summary>
        /// The <see cref="System.Data.Objects.IObjectSet{T}"/> to use.
        /// </summary>
        protected ObjectSet<T> _objectSet;
        /// <summary>
        /// Context used for DAL operations
        /// </summary>
        private IUnitOfWork _unitOfWork;
        /// <summary>
        /// Context accessor
        /// </summary>
        public IUnitOfWork UnitOfWork
        {
            get
            {
                return _unitOfWork;
            }
        }

        /// <summary>  
        /// Initializes a new instance of the GenericRepository class  
        /// </summary>  
        /// <param name="objectContext">The Entity Framework ObjectContext</param>  
        protected GenericRepository(IObjectContext objectContext)
        {
            _unitOfWork = objectContext;
            _objectSet = objectContext.CreateObjectSet<T>();
        }

        /// <summary>
        /// Gets an <see cref="IQueryable{T}"/> to perform further operations.
        /// </summary>
        /// <returns>An <see cref="IQueryable{T}"/>.</returns>
        public IQueryable<T> GetQuery(params string[] includes)
        {
            ObjectQuery<T> objectQuery = _objectSet;

            if (includes != null)
            {
                foreach (var include in includes)
                {
                    objectQuery = objectQuery.Include(include);
                }
            }

            return objectQuery;
        }

        /// <summary>
        /// Gets an <see cref="IEnumerable{T}"/> of all elements.
        /// </summary>
        /// <returns>An <see cref="IEnumerable{T}"/> of all elements.</returns>
        public IEnumerable<T> GetAll(params string[] includes)
        {
            var result = GetQuery(includes);
            return result;
        }

        /// <summary>
        /// Gets an <see cref="IEnumerable{T}"/> of all elements matching the given condition.
        /// </summary>
        /// <param name="criteriaSet">The condition.</param>
        /// <param name="includes"></param>
        /// <returns>An <see cref="IEnumerable{T}"/> of all elements matching the given condition.</returns>
        public IEnumerable<T> Find(Expression<Func<T, bool>> where, params string[] includes)
        {
            var result = GetQuery(includes).Where(where);
            return result;
        }

        /// <summary>
        /// Gets a single instance matching the given condition.
        /// </summary>
        /// <param name="where">The condition.</param>
        /// <param name="includes"></param>
        /// <returns>A single instance matching the given condition.</returns>
        public T Single(Expression<Func<T, bool>> where, params string[] includes)
        {
            var result = GetQuery(includes).Single(where);
            return result;
        }

        /// <summary>
        /// Gets a single instance matching the given condition or null if condition does not match.
        /// </summary>
        /// <param name="where">The condition.</param>
        /// <param name="includes"></param>
        /// <returns>A single instance matching the given condition or <c>null</c> if condition does not match.</returns>
        public T SingleOrDefault(Expression<Func<T, bool>> where, params string[] includes)
        {
            var result = GetQuery(includes).SingleOrDefault(where);
            return result;
        }

        /// <summary>
        /// Adds the given entity.
        /// </summary>
        /// <param name="entity">The entity to add.</param>
        public void Add(T entity)
        {
            _objectSet.AddObject(entity);
        }

        /// <summary>
        /// Updates the the given entity.
        /// </summary>
        /// <param name="entity">The entity to update.</param>
        public void Update(T entity)
        {
            _objectSet.Attach(entity);
            _objectSet.Context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
        }

        /// <summary>
        /// Saves the the given entity along with its child relations.
        /// All Create/Update/Delete operations are performed as requred for each entity in the object graph.
        /// </summary>
        /// <param name="entity">The entity to save.</param>
        public void Save(T entity)
        {
            // Detach the adding child entities
            var addingEntities = new Dictionary<string, List<ParentChildEntityPair>>();
            DetachAddingEntities(entity, addingEntities);

            if (entity.ChangeTracker.State == ObjectState.Added)
            {
                // Add object with its graph
                _objectSet.AddObject(entity);
            }
            else
            {
                // Attach object with its graph
                _objectSet.Attach(entity);
            }

            if (entity.ChangeTracker.State != ObjectState.Added)
            {
                // Update the state of the modified objects in the context
                foreach (var entityToUpdate in _objectSet.Context.ObjectStateManager.GetObjectStateEntries(EntityState.Unchanged)
                    .Where(entry => !entry.IsRelationship)
                    .Select(entry => (IObjectWithChangeTracker)entry.Entity)
                    .Where(e => e.ChangeTracker.State == ObjectState.Modified ||
                    e.ChangeTracker.State == ObjectState.Deleted))
                {
                    if (entityToUpdate.ChangeTracker.State == ObjectState.Modified)
                    {
                        _objectSet.Context.ObjectStateManager.ChangeObjectState(entityToUpdate, EntityState.Modified);
                    }

                    if (entityToUpdate.ChangeTracker.State == ObjectState.Deleted)
                    {
                        _objectSet.Context.ObjectStateManager.ChangeObjectState(entityToUpdate, EntityState.Deleted);
                    }
                }

                if (entity.ChangeTracker.State == ObjectState.Modified || entity.ChangeTracker.State == ObjectState.Unchanged)
                {
                    // Add the adding entities back to their parent collections
                    foreach (KeyValuePair<string, List<ParentChildEntityPair>> childEntityCollection in addingEntities)
                    {
                        foreach (ParentChildEntityPair parentChildEntityPair in childEntityCollection.Value)
                        {
                            PropertyInfo childCollectionProperty = parentChildEntityPair.ParentEntity.GetType().GetProperty(childEntityCollection.Key);

                            if (childCollectionProperty != null)
                            {
                                object childCollection = childCollectionProperty.GetValue(parentChildEntityPair.ParentEntity, null);

                                if (childCollection != null)
                                {
                                    childCollection.GetType().InvokeMember("Add", BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, childCollection, new object[] { parentChildEntityPair.ChildEntity });
                                    var contextCollectionProperty = _objectSet.Context
                                        .GetType()
                                        .GetProperties()
                                        .Where(prop => prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericArguments().First().Name == parentChildEntityPair.ChildEntity.GetType().Name)
                                        .FirstOrDefault();

                                    if (contextCollectionProperty != null)
                                    {
                                        _objectSet.Context.AddObject(contextCollectionProperty.Name, parentChildEntityPair.ChildEntity);
                                        _objectSet.Context.ObjectStateManager.ChangeObjectState(parentChildEntityPair.ChildEntity, EntityState.Added);
                                    }
                                }
                            }
                        }
                    }
                }

                // Delete the child entities marked for deletion
                RemoveDeletingEntities(entity);
            }
        }

        /// <summary>
        /// Deletes the given entity.
        /// </summary>
        /// <param name="entity">The entity to delete.</param>
        public void Delete(T entity)
        {
            _objectSet.Attach(entity);
            _objectSet.Context.ObjectStateManager.ChangeObjectState(entity, EntityState.Deleted);
        }

        /// <summary>
        /// Deletes the given entity.
        /// </summary>
        /// <param name="where">The condition.</param>
        public void Delete(Expression<Func<T, bool>> where)
        {
            T entity = _objectSet.Single(where);
            _objectSet.DeleteObject(entity);
        }

        /// <summary>
        /// Exectues a stored procedure of function in the database
        /// </summary>
        /// <param name="storedProcedureName"></param>
        /// <param name="parameters"></param>
        /// <returns>The number of rows affected</returns>
        public int ExecuteStoredProcedure(string storedProcedureName, Dictionary<string, object> parameters)
        {
            IList<ObjectParameter> parametersCollection = new List<ObjectParameter>();

            if (parameters != null)
            {
                foreach (var parameter in parameters)
                {
                    parametersCollection.Add(new ObjectParameter(parameter.Key, parameter.Value));
                }
            }

            return _objectSet.Context.ExecuteFunction(storedProcedureName, parametersCollection.ToArray());
        }

        #region Helper Methods
        /// <summary>
        /// Removes all the adding entities from the provided entity object graph and adds them to the provided collection
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="addingEntities"></param>
        private void DetachAddingEntities(IObjectWithChangeTracker entity, Dictionary<string, List<ParentChildEntityPair>> addingEntities)
        {
            if (entity.ChangeTracker.State == ObjectState.Modified || entity.ChangeTracker.State == ObjectState.Unchanged)
            {
                if (entity.ChangeTracker.ObjectsAddedToCollectionProperties.Count != 0)
                {
                    // Clear object graph from child entities that are in Add state, otherwise the attach to context is not possible
                    // First, save the child adding entities to a new collection
                    foreach (KeyValuePair<string, ObjectList> childEntityCollection in entity.ChangeTracker.ObjectsAddedToCollectionProperties)
                    {
                        if (!addingEntities.ContainsKey(childEntityCollection.Key))
                        {
                            addingEntities.Add(childEntityCollection.Key, new List<ParentChildEntityPair>());
                        }

                        foreach (object childEntity in childEntityCollection.Value)
                        {
                            addingEntities[childEntityCollection.Key].Add(new ParentChildEntityPair(entity, childEntity as IObjectWithChangeTracker));
                        }
                    }

                    // Remove the adding entities from the collection of child entities
                    foreach (KeyValuePair<string, List<ParentChildEntityPair>> childEntityCollection in addingEntities)
                    {
                        foreach (ParentChildEntityPair parentChildPair in childEntityCollection.Value)
                        {
                            PropertyInfo childCollectionProperty = entity.GetType().GetProperty(childEntityCollection.Key);

                            if (childCollectionProperty != null)
                            {
                                object childCollection = childCollectionProperty.GetValue(entity, null);

                                if (childCollection != null)
                                {
                                    childCollection.GetType().InvokeMember("Remove", BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, childCollection, new object[] { parentChildPair.ChildEntity });
                                }
                            }
                        }
                    }
                }

                // Recurse through the next level of child entities
                IEnumerable<PropertyInfo> childCollectionProperties = entity.GetType().GetProperties().Where(p => p.PropertyType.Name == "TrackableCollection`1");

                foreach (PropertyInfo childCollectionProperty in childCollectionProperties)
                {
                    IEnumerable childCollection = entity.GetType().GetProperty(childCollectionProperty.Name).GetValue(entity, null) as IEnumerable;

                    foreach (object item in childCollection)
                    {
                        IObjectWithChangeTracker childEntity = item as IObjectWithChangeTracker;

                        if (childEntity.ChangeTracker.State == ObjectState.Modified || childEntity.ChangeTracker.State == ObjectState.Unchanged)
                        {
                            DetachAddingEntities(childEntity, addingEntities);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Removes the child entities marked from deletion from the object graph of the provided parent
        /// </summary>
        /// <param name="entity"></param>
        private void RemoveDeletingEntities(IObjectWithChangeTracker entity)
        {
            if (entity.ChangeTracker.State == ObjectState.Modified || entity.ChangeTracker.State == ObjectState.Unchanged)
            {
                var removingEntities = entity.ChangeTracker.ObjectsRemovedFromCollectionProperties.ToList();

                for (int i = removingEntities.Count - 1; i >= 0; i--)
                {
                    for (int j = removingEntities[i].Value.Count - 1; j >= 0; j--)
                    {
                        var deletingEntity = removingEntities[i].Value[j];
                        _objectSet.Context.AttachTo(removingEntities[i].Key, deletingEntity);
                        _objectSet.Context.ObjectStateManager.ChangeObjectState(deletingEntity, EntityState.Deleted);
                    }
                }

                // Recurse through the next level of child entities
                IEnumerable<PropertyInfo> childCollectionProperties = entity.GetType().GetProperties().Where(p => p.PropertyType.Name == "TrackableCollection`1");

                foreach (PropertyInfo childCollectionProperty in childCollectionProperties)
                {
                    IEnumerable childCollection = entity.GetType().GetProperty(childCollectionProperty.Name).GetValue(entity, null) as IEnumerable;

                    foreach (object item in childCollection)
                    {
                        IObjectWithChangeTracker childEntity = item as IObjectWithChangeTracker;

                        if (childEntity.ChangeTracker.State == ObjectState.Modified || childEntity.ChangeTracker.State == ObjectState.Unchanged)
                        {
                            RemoveDeletingEntities(childEntity);
                        }
                    }
                }
            }
        }
        #endregion

        #region Inner Types
        /// <summary>
        /// Container for a parent and child entity association
        /// </summary>
        public class ParentChildEntityPair
        {
            /// <summary>
            /// Constructor
            /// </summary>
            /// <param name="parent"></param>
            /// <param name="child"></param>
            public ParentChildEntityPair(IObjectWithChangeTracker parent, IObjectWithChangeTracker child)
            {
                ParentEntity = parent;
                ChildEntity = child;
            }

            public IObjectWithChangeTracker ParentEntity { get; set; }
            public IObjectWithChangeTracker ChildEntity { get; set; }
        }
        #endregion
    }
}
